/**
 * Copyright 2009 University of  Washington
 * This code is released under the GPL version 3 - with a copy of the license included
 * @author Maya Hao Li
 *
 */
package seedpod.rdb;

/**
 * Shared connection pool to the database.
 */

import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Vector;

public class RdbConnection implements Runnable {

	public static final int MAX_CONNECTIONS = 50;

	private RdbConnectParameters _param;
	private Vector<Connection> _availableConnections, _busyConnections;
	private static RdbConnection _instance;
	private boolean _connectionPending = false;
	private static boolean _isInstantiated = false;

	private RdbConnection() {
		init();
	}

	public void setConnectionParameters( RdbConnectParameters p ) {
		instance()._param = (RdbConnectParameters)p.clone();
	}

	/**
	 * WARNING: current implementation of RdbConnection is a singleton class which accepts only one connection
	 * parameter. To use these public constructor would actually change all the connection for the application.
	 * 
	 * @param p
	 */
	// private RdbConnection(RdbConnectParameters p) {
	// _param = (RdbConnectParameters)p.clone();
	// init();
	// }
	//
	// private RdbConnection(String driver, String url, String user, String pwd, String database) {
	// _param = new RdbConnectParameters( driver, url, user, pwd, database );
	// init();
	// }
	private void init() {
		_availableConnections = new Vector<Connection>();
		_busyConnections = new Vector<Connection>();
		_isInstantiated = false;

		if ( !_isInstantiated ) {
			_instance = this;
			_isInstantiated = true;
		}
	}

	public static synchronized RdbConnection instance() {
		if ( _instance != null )
			return _instance;
		else {
			_isInstantiated = false;
			return new RdbConnection();
		}
	}

	public synchronized Connection getConnection() throws SQLException {
		if ( !_availableConnections.isEmpty() ) {
			Connection existingConnection = (Connection)_availableConnections.lastElement();
			int lastIndex = _availableConnections.size() - 1;
			_availableConnections.removeElementAt( lastIndex );

			if ( existingConnection.isClosed() ) {
				notifyAll();
				return (getConnection());
			} else {
				_busyConnections.addElement( existingConnection );
				return existingConnection;
			}
		} else {
			if ( (totalConnections() < MAX_CONNECTIONS) && !_connectionPending ) {
				makeBackgroundConnection();
			} else {
				try {
					wait();
				}
				catch (InterruptedException ie) {
					System.err.println( "interruptedException:waiting for db connection" );
				}
			}
			return getConnection();
		}
	}

	public synchronized int totalConnections() {
		return (_availableConnections.size() + _busyConnections.size());
	}

	private void makeBackgroundConnection() {
		_connectionPending = true;
		try {
			Thread connectThread = new Thread( this );
			connectThread.start();
		}
		catch (OutOfMemoryError oome) {
			System.err.println( "OutOfMemoryException in makeBackgroundConnect" );
		}
	}

	public void run() {
		try {
			Connection connection = makeNewConnection();
			synchronized (this) {
				instance()._availableConnections.addElement( connection );
				instance()._connectionPending = false;
				notifyAll();
			}
		}
		catch (Throwable e) {
			e.printStackTrace();
		}
	}

	private Connection makeNewConnection() throws SQLException {
		try {
			Class.forName( _param.getDriverName() );
			System.err.println( "Make new connection: " + toString() );

			// Establish a shared connection to the class database.
			Connection connection = DriverManager.getConnection( _param.getJdbcUrl(), _param.getDbUser(), _param
					.getDbPassword() );

			return connection;
		}
		catch (ClassNotFoundException e) {
			System.err.println( "error in make new connection: " + toString() );
			throw new SQLException( "class not found: " + _param.getJdbcUrl() );
		}
	}

	public synchronized void free( Connection connection ) {
		System.err.println( toString() );

		_busyConnections.removeElement( connection );
		_availableConnections.addElement( connection );
		notifyAll();
		System.err.println( toString() );

	}

	public synchronized void closeAllConnections() {
		closeConnections( _availableConnections );
		_availableConnections = new Vector<Connection>();
		closeConnections( _busyConnections );
		_busyConnections = new Vector<Connection>();
	}

	private void closeConnections( Vector<Connection> connections ) {
		try {
			for ( int i = 0; i < connections.size(); i++ ) {
				Connection connection = (Connection)connections.elementAt( i );
				if ( !connection.isClosed() ) {
					connection.close();
				}
			}
		}
		catch (SQLException sqle) {}
	}

	public synchronized String toString() {
		String info = "DBConnection(" + _param.getJdbcUrl() + " , " + _param.getDbUser() + " ) " + ", available="
				+ _availableConnections.size() + ", busy= " + _busyConnections.size() + ", max = " + MAX_CONNECTIONS;
		return info;
	}

	/**
	 * Returns a Statement. First checks to see if we have a connection object. If not, sets one up. If yes, then gets a
	 * statement and returns it.
	 * 
	 * @throws SQLException
	 * @returns a Statement object
	 */
	public Statement getStatement() throws SQLException {
		Statement stmt = null;

		Connection connection = instance().getConnection();

		// get the statement
		try {
			stmt = connection.createStatement();
		}
		catch (SQLException e) {
			e.printStackTrace();
			connection.close();
			e.printStackTrace( System.err );
		}
		return stmt;
	}

	public PreparedStatement prepareStatement( String sql ) throws SQLException {
		Connection conn = instance().getConnection();
		return conn.prepareStatement( sql );
	}

	public static DatabaseMetaData getDBMetaData() throws SQLException {
		Connection connection = instance().getConnection();
		return connection.getMetaData();
	}

	public RdbConnectParameters getRdbParameters() {
		return _param;
	}

	public int executeUpdate( String u ) throws SQLException {
		Statement stmt = getStatement();
		int result = stmt.executeUpdate( u );
		dispose( stmt );
		free( stmt );
		return result;
	}

	// ** Executes the query given and returns the ResultSet. *
	public ResultSet executeQuery( String q ) throws SQLException {
		Statement stmt = getStatement();
		ResultSet rs = null;

		rs = stmt.executeQuery( q );
		free( stmt ); // free up Connection
		return rs;
	}

	public ResultSet executePreparedStatementQuery( StringBuffer sb ) throws SQLException {
		PreparedStatement stmt = prepareStatement( sb.toString() );
		ResultSet rs = null;
		rs = stmt.executeQuery();
		free( stmt );
		return rs;
	}

	public boolean execute( String q ) throws SQLException {
		Statement stmt = getStatement();
		boolean success = stmt.execute( q );
		free( stmt );
		return success;
	}

	/**
	 * frees a Connection associated with the statement note that the statement remains open
	 * 
	 * @param s Statement that holds a connection to the database
	 */
	private static void free( Statement s ) {
		try {
			instance().free( s.getConnection() );

		}
		catch (SQLException sqle) {
			System.err.println( "Unable ot free statement & connection" );
			sqle.printStackTrace();
		}
	}

	/**
	 * Clean-up method. Closes the statement.
	 * 
	 * @param s that's to be closed
	 */
	private static void dispose( Statement s ) {
		try {
			s.close();
		}
		catch (SQLException e) { // ignore
		}
	}
}